/*
 * DocumentWindow.java
 * Describe SWF
 *
 * A browser for Flash files.
 * Copyright (c) 2001-2006 Flagstone Software Ltd. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
 * Place, Suite 330, Boston, MA 02111-1307 USA
 * 
 * For more information please contact:
 * 
 * Stuart MacKay
 * Flagstone Software Ltd
 * 92 High Street
 * Wick, Caithness KW1 4LY
 * Scotland
 * 
 * www.flagstonesoftware.com
 */
package com.flagstone.describe;

import java.awt.*;
import java.awt.event.*;
import java.awt.font.FontRenderContext;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.net.URL;
import java.util.Observable;
import java.util.Observer;

import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import javax.swing.text.*;
import javax.swing.text.html.*;
import javax.swing.tree.*;
import javax.swing.table.*;

class DocumentWindow extends JFrame implements Observer, WindowListener, TreeSelectionListener
{
    /*
     * This custom tree cell renderer is used to turn off the folder and file icons 
     * that are used in the DefaultTreeCellRenderer.
     */
    class DescribeTreeCellRenderer extends DefaultTreeCellRenderer
    {
        private static final long serialVersionUID = 0;
        
        DescribeTreeCellRenderer()
        {
            super();        
            setLeafIcon(null);
            setClosedIcon(null);
            setOpenIcon(null);
        }    
    }    
    class BinaryModel extends AbstractTableModel 
    {
        private static final long serialVersionUID = 0;
        
        String columnNames[] = new String[] {
            "Addr", 
            "7654 3210 7654 3210 7654 3210 7654 3210", 
            "00 01 02 03"
        };
        
        BinaryModel()
        {
            super();
        }
        public int getColumnCount() 
        { 
            return columnNames.length;
        }
        public int getRowCount() 
        {
            int count = 0;
            
            if (document.getFileData() != null)
            {
                count = document.getFileData().length/4;
                
                if (document.getFileData().length % 4 > 0)
                    count += 1;
            }
            return count;
        }
        public String getColumnName(int col) 
        {
            return columnNames[col];
        }
        public Object getValueAt(int row, int col) 
        {
            String value = null;

            switch (col)
            {
            case 0:
                int prefixLength = getColumnWidth(col);
                
                for (int i=0; i<prefixLength; i++)
                    value += "0";
                
                value += Integer.toHexString(row*4).toUpperCase();
                
                value = value.substring(value.length()-prefixLength);
                break;
            case 1:
                value = document.binaryString(row*4, 4, 4);
                break;
            case 2:
                value = document.hexString(row*4, 4, 1);
               break;
            default:
                break;
            }
            return value; 
        }
        public Class getColumnClass(int c) 
        {
            return String.class;
        }
        public int getColumnWidth(int c)
        {
            int width = 0;
            
            if (c == 0)
                width = (int)(Math.log(getRowCount()*4)/Math.log(16)+1);
            else
                width = widthOfString(columnNames[c]);
            
            return width;
        }
        public void update()
        {
            fireTableDataChanged();
        }
    }
    class HexModel extends AbstractTableModel 
    {
        private static final long serialVersionUID = 0;
        
        String columnNames[] = new String[] {
            "Addr", 
            "0001 0203 0405 0607 0809 0A0B 0C0D 0E0F", 
            "0123456789ABCDEF"
        };

        HexModel()
        {
            super();
        }
        public int getColumnCount() 
        { 
            return columnNames.length;
        }
        public int getRowCount() 
        {
            int count = 0;
            
            if (document.getFileData() != null)
            {
                count = document.getFileData().length/16;

                if (document.getFileData().length % 16 > 0)
                    count += 1;
            }
                
            return count;
        }
        public String getColumnName(int col) 
        {
            return columnNames[col];
        }
        public Object getValueAt(int row, int col) 
        {
            String value = null;
            
            switch (col)
            {
            case 0:
                int prefixLength = getColumnWidth(col);
                
                for (int i=0; i<prefixLength; i++)
                    value += "0";
                
                value += Integer.toHexString(row*16).toUpperCase();
                
                value = value.substring(value.length()-prefixLength);
                break;
            case 1:
                value = document.hexString(row*16, 16, 2);
                break;
            case 2:
                value = document.characterString(row*16, 16);
               break;
            default:
                break;
            }
            
            return value; 
        }
        public Class getColumnClass(int c) 
        {
            return String.class;
        }
        public int getColumnWidth(int c)
        {
            int width = 0;
            
            if (c == 0)
                width = (int)(Math.log(getRowCount()*16)/Math.log(16)+1);
            else
                width = widthOfString(columnNames[c]);
            
            return width;
        }
        public void update()
        {
            fireTableDataChanged();
        }
    }
    class HexStringRenderer implements TableCellRenderer 
    {
        private static final long serialVersionUID = 0;
            
        private int startRow = 0;
        private int valueStartCol = 0;
//        private int strStartCol = 0;
        private int endRow = 0;
        private int valueEndCol = 0;
//        private int strEndCol = 0;
        
        JTextField address = new JTextField();
        JTextField values = new JTextField();
        JTextField string = new JTextField();

        /*
         * Left side of cell border is set to 3 pixels so text aligns 
         * correctly with the title in the table header.
         */
        public HexStringRenderer() 
        {
            super();
            
            address.setOpaque(true);
            address.setFont(Mono12);
            address.setBackground(SystemColor.control);
            address.setBorder(new EmptyBorder(0,3,0,3));
            address.setHorizontalAlignment(JTextField.RIGHT);

            values.setOpaque(true);
            values.setFont(Mono12);
            values.setBorder(new EmptyBorder(0,3,0,0));
            values.setHorizontalAlignment(JTextField.LEFT);

            string.setOpaque(true);
            string.setFont(Mono12);
            string.setBorder(new EmptyBorder(0,3,0,0));
            string.setHorizontalAlignment(JTextField.LEFT);
        }
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) 
        {
            JTextField field = null;
            
            setEnabled(table.isEnabled());
            
            if (column == 0) {
                field = address;
            }
            else if (column == 1) {
                field = values;
            }
            else {
                field = string;
            }
            
            field.setText((String)value);
   
            if (column == 1)
            {
                boolean highlight = false;
                
                int p0 = 0;
                int p1 = 0;
                
                if (row == startRow)
                {
                    p0 = valueStartCol;
                    p1 = (endRow > startRow) ? 39 : valueEndCol;
                    highlight = true;
                }
                else if (row > startRow && row < endRow)
                {
                    p0 = 0;
                    p1 = 39;
                    highlight = true;
                }
                else if (row == endRow)
                {
                    p0 = 0;
                    p1 = valueEndCol;
                    highlight = true;
                }
                
                if (highlight)
                {
                    try
                    {
                        values.getHighlighter().addHighlight(p0, p1, DefaultHighlighter.DefaultPainter);
                    }
                    catch (Exception e)
                    {    
                    }
                }
            }
            
/* Unused - for now          
            
            else if (column == 2)
            {
                boolean highlight = false;
                
                int p0 = 0;
                int p1 = 0;
                
                if (row == startRow)
                {
                    p0 = strStartCol;
                    p1 = (endRow > startRow) ? 16 : strEndCol;
                    highlight = true;
                }
                else if (row > startRow && row < endRow)
                {
                    p0 = 0;
                    p1 = 16;
                    highlight = true;
                }
                else if (row == endRow)
                {
                    p0 = 0;
                    p1 = strEndCol;
                    highlight = true;
                }
                
                if (highlight)
                {
                    try
                    {
                        string.getHighlighter().addHighlight(p0, p1, DefaultHighlighter.DefaultPainter);
                    }
                    catch (Exception e)
                    {    
                    }
                }
            }
*/
            return field;
        }
    }
    class BinaryStringRenderer implements TableCellRenderer 
    {
        private static final long serialVersionUID = 0;
        
        private int startRow = 0;
        private int valueStartCol = 0;
//        private int strStartCol = 0;
        private int endRow = 0;
        private int valueEndCol = 0;
//        private int strEndCol = 0;

        JTextField address = new JTextField();
        JTextField values = new JTextField();
        JTextField string = new JTextField();

        /*
         * Left side of cell border is set to 3 pixels so text aligns 
         * correctly with the title in the table header.
         */
        public BinaryStringRenderer() 
        {
            super();
            
            address.setOpaque(true);
            address.setFont(Mono12);
            address.setBackground(SystemColor.control);
            address.setBorder(new EmptyBorder(0,3,0,3));
            address.setHorizontalAlignment(JTextField.RIGHT);

            values.setOpaque(true);
            values.setFont(Mono12);
            values.setBorder(new EmptyBorder(0,3,0,0));
            values.setHorizontalAlignment(JTextField.LEFT);

            string.setOpaque(true);
            string.setFont(Mono12);
            string.setBorder(new EmptyBorder(0,3,0,0));
            string.setHorizontalAlignment(JTextField.LEFT);
        }
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) 
        {
            JTextField field = null;
            
            setEnabled(table.isEnabled());
            
            if (column == 0) {
                field = address;
            }
            else if (column == 1) {
                field = values;
            }
            else {
                field = string;
            }
            
            field.setText((String)value);
               
            if (column == 1)
            {
                boolean highlight = false;
                
                int p0 = 0;
                int p1 = 0;
                
                if (row == startRow)
                {
                    p0 = valueStartCol;
                    p1 = (endRow > startRow) ? 39 : valueEndCol;
                    highlight = true;
                }
                else if (row > startRow && row < endRow)
                {
                    p0 = 0;
                    p1 = 39;
                    highlight = true;
                }
                else if (row == endRow)
                {
                    p0 = 0;
                    p1 = valueEndCol;
                    highlight = true;
                }
                
                if (highlight)
                {
                    try
                    {
                        values.getHighlighter().addHighlight(p0, p1, DefaultHighlighter.DefaultPainter);
                    }
                    catch (Exception e)
                    {    
                    }
                }
            }
            return field;
        }
    }

    private static Font Helvetica12 = new Font("Helvetica", Font.PLAIN, 12);
    private static Font Mono12 = new Font("Monospaced", Font.PLAIN, 12);
    
    private static final long serialVersionUID = 0;
    
    Document document;
    
    JTree tree = null;
    JTextArea text = null;    
    JTable binary = null;    
    JTable hex = null;
    JTextField status = null;
    JTextField location = null;
    JTabbedPane tabView = null;
    
    BinaryStringRenderer binaryValue = new BinaryStringRenderer();
    HexStringRenderer hexValue = new HexStringRenderer();
    
    JEditorPane description = null;    

    URL currentPage = null;
    
    int[] binarySelection = new int[4];
    int[] hexSelection = new int[4];
    
    DocumentWindow(Image icon, Document doc, MovieNode root)
    {
        document = doc;
        
        setIconImage(icon);
        addWindowListener(this);
        doc.addObserver(this);

        setSize(750, 550);
         
        // Initialize the text areas with default values for the application.
        tree = new JTree();
        tree.setCellRenderer(new DescribeTreeCellRenderer());
        tree.setModel(new DefaultTreeModel(root));
        tree.addTreeSelectionListener(this);
        tree.putClientProperty("JTree.lineStyle", "Angled");
        tree.setEnabled(false);

        text = new JTextArea();
        text.setFont(Helvetica12);
        text.setLineWrap(true);
        text.setWrapStyleWord(true);
        text.setTabSize(2);
        text.setSelectedTextColor(Color.blue);
        text.setEditable(false);
        text.setCaretPosition(0);
        
        BinaryModel binModel = new BinaryModel();
        
        binary = new JTable();
        binary.setFont(Mono12);
        binary.getTableHeader().setFont(Mono12);
        binary.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        binary.setModel(binModel);
        binary.setDefaultRenderer(Object.class, binaryValue);

        binary.getColumnModel().getColumn(1).setPreferredWidth(binModel.getColumnWidth(1));
        binary.getColumnModel().getColumn(2).setPreferredWidth(binModel.getColumnWidth(2));
        
        HexModel hexModel = new HexModel();
         
        hex = new JTable();
        hex.setFont(Mono12);
        hex.getTableHeader().setFont(Mono12);
        hex.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        hex.setModel(hexModel);
        hex.setDefaultRenderer(Object.class, hexValue);
        
        hex.getColumnModel().getColumn(1).setPreferredWidth(hexModel.getColumnWidth(1));
        hex.getColumnModel().getColumn(2).setPreferredWidth(hexModel.getColumnWidth(2));


        description = new JEditorPane("text/html", "");
        description.setEditable(false);
 
        HTMLEditorKit editorKit = (HTMLEditorKit)description.getEditorKit();
        URL sheet = getClass().getResource("/descriptions/stylesheet.css");
        StyleSheet stylesheet = new StyleSheet();
        stylesheet.importStyleSheet(sheet);
        editorKit.setStyleSheet(stylesheet);
        
        // Initialize the tabbed view with the text areas
        
        tabView = new JTabbedPane();
        tabView.setMinimumSize(new Dimension(300, 300));
        tabView.setEnabled(false);
        
        tabView.addTab("Text", new JScrollPane(text));
        tabView.addTab("Binary", new JScrollPane(binary));
        tabView.addTab("Hex", new JScrollPane(hex));
        tabView.addTab("Description", new JScrollPane(description));
        
        JPanel outlineView = new JPanel();
        outlineView.setLayout(new BorderLayout());
        outlineView.add(new JScrollPane(tree), "Center");
        outlineView.setMinimumSize(new Dimension(200, 300));
        
        status = new JTextField(30);
        status.setText("Open Document");
        status.setBorder(new EmptyBorder(0,3,0,0));
        status.setBackground(SystemColor.control);
        status.setSize(status.getPreferredSize());
        status.setMinimumSize(status.getPreferredSize());
        status.setMaximumSize(status.getPreferredSize());
        status.setEditable(false);
        
        location = new JTextField(15);
        location.setText("0 : 0");
        location.setBorder(new EmptyBorder(0,3,0,0));
        location.setBackground(SystemColor.control);
        location.setSize(location.getPreferredSize());
        location.setMinimumSize(location.getPreferredSize());
        location.setMaximumSize(location.getPreferredSize());
        location.setEditable(false);

        FlowLayout layout = new FlowLayout(FlowLayout.LEFT);
        layout.setHgap(2);
        layout.setVgap(2);
        
        JSeparator left = new JSeparator(javax.swing.SwingConstants.VERTICAL);
        left.setPreferredSize(new Dimension(5,15));
        JSeparator right = new JSeparator(javax.swing.SwingConstants.VERTICAL);
        right.setPreferredSize(new Dimension(5,15));

        JPanel statusPanel = new JPanel();
        statusPanel.setLayout(layout);        
        statusPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
        statusPanel.add(status);
        statusPanel.add(left);
        statusPanel.add(location);
        statusPanel.add(right);
        statusPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        
        JSplitPane splitView = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, 
                        outlineView, tabView);
        splitView.setAlignmentX(Component.LEFT_ALIGNMENT);

        getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));
        getContentPane().add(splitView);
        getContentPane().add(statusPanel);
    }
    public void update(Observable t, Object o)
    {
        if (o instanceof MovieNode)
        {
            ((BinaryModel)binary.getModel()).update();        
            ((HexModel)hex.getModel()).update();
            
            binary.getColumnModel().getColumn(0).setPreferredWidth(((BinaryModel)binary.getModel()).getColumnWidth(0));
            hex.getColumnModel().getColumn(0).setPreferredWidth(((HexModel)hex.getModel()).getColumnWidth(0));

            TreePath path = tree.getPathForRow(0);
            
            if (tree.isExpanded(path) == false)
                tree.expandPath(path);
            if (tree.isEnabled() == false)
                tree.setEnabled(true);
            if (tabView.isEnabled() == false)
                tabView.setEnabled(true);
        }
    }
    /*
     * Tree Listener Methods
     */
    public void valueChanged(TreeSelectionEvent e) 
    { 
        final MovieNode selectedNode = (MovieNode)e.getPath().getLastPathComponent();
        
        if (selectedNode != null)
        {
            int option = JOptionPane.OK_OPTION;
                
            if (selectedNode.isRoot())
            {
                option = JOptionPane.showConfirmDialog(null, 
                 App.getInstance().localizedString("DisplayRootNode"), 
                 App.getInstance().localizedString("DisplayRootNodeTitle"), JOptionPane.YES_NO_OPTION);
            }
            
            if (option == JOptionPane.OK_OPTION)
            {
                showText(selectedNode);
                showLocation(selectedNode);
                showDescription(selectedNode);
            }
            else
            {
                text.setText("");
                text.setCaretPosition(0);
                
                if (selectedNode.isLeaf())
                    status.setText(document.describeNode(selectedNode));
                else
                    status.setText(selectedNode.displayName());
                
                location.setText("0x"+(Integer.toHexString((selectedNode.getLocation() >>> 3)).toUpperCase()) + " : " + (selectedNode.getLength() >>> 3));    

                showLocation(selectedNode);
                showDescription(selectedNode);
            }
        }
    }
    
    private void showText(MovieNode aNode)
    {
        try
        {
            text.setText(document.describeNode(aNode));
            text.setCaretPosition(0);
            
            if (aNode.isLeaf())
                status.setText(document.describeNode(aNode));
            else
                status.setText(aNode.displayName());
            
            location.setText("0x"+(Integer.toHexString((aNode.getLocation() >>> 3)).toUpperCase()) + " : " + (aNode.getLength() >>> 3));    
        }
        catch (Error e)
        {
            JOptionPane.showMessageDialog(null, App.getInstance().localizedString("Error.OutOfMemory"),
                App.getInstance().localizedString("Error.OutOfMemoryTitle"), JOptionPane.ERROR_MESSAGE);
            
            System.gc();
            
            text.setText("");
            text.setCaretPosition(0);
            
            if (aNode.isLeaf())
                status.setText(document.describeNode(aNode));
            else
                status.setText(aNode.displayName());
            
            location.setText("0x"+(Integer.toHexString((aNode.getLocation() >>> 3)).toUpperCase()) + " : " + (aNode.getLength() >>> 3));    
        }
    }
    private void showLocation(MovieNode aNode)
    {
        int startBit = aNode.getLocation();
        int numberOfBits = aNode.getLength();
        int endBit = startBit + numberOfBits;
       
        int startByte = startBit >>> 3;
        
        int numberOfBytes = numberOfBits >>> 3;
        
        if (numberOfBits % 8 > 0)
            numberOfBytes += 1;
        
        int endByte = startByte + numberOfBytes;
        
        int offset = 0;
        
        hexValue.startRow = startByte/16;
        hexValue.endRow = endByte/16;        

        offset = startByte%16;
//        hexValue.strStartCol = offset;
        hexValue.valueStartCol = offset*2 + offset/2;
        
        offset = endByte%16;
//        hexValue.strEndCol = offset;
        hexValue.valueEndCol = offset*2 + (offset-1)/2;
        
        binaryValue.startRow = startBit/32;
        binaryValue.endRow = endBit/32;        

        offset = startBit%32;
//        binaryValue.strStartCol = offset;
        binaryValue.valueStartCol = offset + offset/4;
        
        offset = endBit%32;
//        binaryValue.strEndCol = offset;
        binaryValue.valueEndCol = offset + (offset-1)/4;
        
        update(null, aNode);
    }
    private void showDescription(MovieNode node)
    {
        URL nextPage = document.infoForNode(node);
        
        try
        {
            if (nextPage != null && nextPage.equals(currentPage) == false)
            {
                description.setPage(nextPage);
                description.scrollToReference(node.getName());
                currentPage = nextPage;
            }
        }
        catch (IOException e)
        {
        }
    }
    private int widthOfString(String str)
    {
        FontRenderContext fontContext = new FontRenderContext(new AffineTransform(), true, true);
        Rectangle2D bounds = Mono12.getStringBounds(str, fontContext);
        
        return (int)bounds.getWidth();
    }
    /*
     * WindowListener Interface Methods
     */
    public void windowClosed(WindowEvent e) {} 
    public void windowIconified(WindowEvent e) {}
    public void windowOpened(WindowEvent e) {}
    public void windowClosing(WindowEvent e)
    {        
        document.close();     
    }
    public void windowDeiconified(WindowEvent e) {}
    public void windowActivated(WindowEvent e) {}
    public void windowDeactivated(WindowEvent e) {}
}
